package com.pine.app.module.security.core.web.xss;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.owasp.validator.html.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.Iterator;
import java.util.regex.Pattern;

/**
 * Xss检查器
 */
public class XssDetector {

    private static final Logger logger = LoggerFactory.getLogger(XssDetector.class);

    private static final String DEFAULT_POLICY = "/antisamy/antisamy.xml";

    /**
     * 严格检查器.
     * 不允许script iframe img video等，允许p a
     */
    private static final XssDetector STRICT_DETECTOR;

    /**
     * 普通检查器
     * 不允许script iframe. 允许img, video, p, a等大部分Html标签， 但不允许其中带有XSS攻击模式
     */
    private static final XssDetector NORMAL_DETECTOR;

    private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL;

    // XSS 敏感模式
    private static final Pattern P_SCRIPT = Pattern.compile("((<|</|<.+(\\s|\\x5cx)+)(script))|(script>)", REGEX_FLAGS_SI);
    private static final Pattern P_JAVASCRIPT = Pattern.compile("(xlink|javascript|vbscript|livescript)(:|(&colon;))", REGEX_FLAGS_SI);
    private static final Pattern P_ONEVENT = Pattern.compile("(<|\\[).*\\son\\w+=", REGEX_FLAGS_SI);
    private static final Pattern P_IFRAME = Pattern.compile("<iframe|<.*\\s+iframe", REGEX_FLAGS_SI);
    private static final Pattern P_OBJECT = Pattern.compile("<object|<.*\\s+object", REGEX_FLAGS_SI);
    private static final Pattern P_EXPRESSION = Pattern.compile("expression\\(", REGEX_FLAGS_SI);
    private static final Pattern P_UTF7 = Pattern.compile("x-imap4-modified-utf7|utf-7", REGEX_FLAGS_SI);
    private static final Pattern P_EMBED = Pattern.compile("<.*embed", REGEX_FLAGS_SI);
    private static final Pattern P_CSS_SCRIPT = Pattern.compile("(mocha|behavior|behaviour|binding)(:|(&colon;))", REGEX_FLAGS_SI);
    private static final Pattern P_TAG_SEMICOLON = Pattern.compile("<;", REGEX_FLAGS_SI);
    private static final Pattern P_CHAR_ENCODE = Pattern.compile("&#x?\\d+"); //&#0000106 &#x106
    private static final Pattern P_CSS_IMPORT = Pattern.compile("(@|\\?)import");
    private static final Pattern P_DOCUMENT_CALL = Pattern.compile("(window|document|cookie)\\.");

    private static final Pattern[] DANGER_PATTERNS = new Pattern[]{P_SCRIPT, P_JAVASCRIPT,
            P_ONEVENT, P_IFRAME, P_OBJECT, P_EXPRESSION, P_UTF7, P_EMBED,
            P_CSS_SCRIPT, P_TAG_SEMICOLON, P_CHAR_ENCODE, P_CSS_IMPORT, P_DOCUMENT_CALL};

    static {
        STRICT_DETECTOR = new XssDetector("/antisamy/antisamy-tinymce.xml");
        NORMAL_DETECTOR = new XssDetector("/antisamy/antisamy.xml");
    }

    private AntiSamy antiSamy = null;

    protected XssDetector(String policyPath) {
        try {
            URL policyUrl = XssDetector.class.getResource(policyPath);
            if (policyUrl == null) {
                policyUrl = XssDetector.class.getResource(DEFAULT_POLICY);
            }
            final Policy policy = Policy.getInstance(policyUrl);
            antiSamy = new AntiSamy(policy);
        } catch (PolicyException e) {
            logger.error("policy read error.", e);
        }
    }

    public static XssDetector getStrictDetector() {
        return STRICT_DETECTOR;
    }

    public static XssDetector getNormalDetector() {
        return NORMAL_DETECTOR;
    }

    /**
     * 对html进行过滤，清理其中含有xss风险的文本
     *
     * @param html 原html
     * @return 清理后的html
     */
    public String xssClean(String html) {
        if (antiSamy == null) {
            return html;
        }
        try {
            final CleanResults cr = antiSamy.scan(html);
            // 安全的HTML输出
            return cr.getCleanHTML();
        } catch (ScanException e) {
            logger.error("scan error.", e);
            return html;
        } catch (PolicyException e) {
            logger.error("policy read error.", e);
            return html;
        }
    }

    /**
     * 检查html是否含有xss风险
     *
     * @param html 受检的html
     * @return true:通过检查; false:未通过检查,含有xss风险
     */
    public boolean passXssCheck(String html) {
        return checkText(html);
    }

    /**
     * 检查json串是否含有xss风险
     *
     * @param json 受检的json
     * @return true:通过检查; false:未通过检查,含有xss风险
     */
    public boolean passXssCheckJson(String json) {
        if (StringUtils.isBlank(json)) {
            return true;
        }

        if (JSON.isValidArray(json)) {
            JSONArray jsonArray = JSON.parseArray(json);
            Iterator<Object> it = jsonArray.iterator();
            while (it.hasNext()) {
                Object value = it.next();
                if (value instanceof String) {
                    boolean valuePass = passXssCheckJson((String) value);
                    if (!valuePass) {
                        return false;
                    }
                }
            }
        } else if (JSON.isValidObject(json)) {
            JSONObject jsonObject = JSON.parseObject(json);
            Collection<Object> values = jsonObject.values();
            for (Object value : values) {
                if (value instanceof String) {
                    boolean valuePass = passXssCheckJson((String) value);
                    if (!valuePass) {
                        return false;
                    }
                }
            }
        } else if (!JSON.isValid(json)) {
            // 普通字符串，但带有特殊字符如< >的
            if (!checkText(json)) {
                return false;
            }
        }

        return true;
    }

    /**
     * 检查文本中是否有xss敏感词汇
     *
     * @param html 文本
     * @return false: 未通过检查; true:通过检查
     */
    private boolean checkText(String html) {
        String unescapeHtml = unesacpeForCheck(html);
        String nonSpaceHtml = unescapeHtml.replaceAll("\\s", "");
        for (Pattern pattern : DANGER_PATTERNS) {
            boolean foundDanger = pattern.matcher(html).find()
                    || pattern.matcher(unescapeHtml).find()
                    || pattern.matcher(nonSpaceHtml).find();
            if (foundDanger) {
                logger.warn("found danger pattern in request={}, pattern={}", html, pattern.toString());
                return false;
            }
        }

        return true;
    }

    /**
     * 将html中的特殊字符转义，用于检测html中是否有xss注入
     *
     * @param html html文本
     * @return 转译为便于检测的文本
     */
    private static String unesacpeForCheck(String html) {
        html = html.replace("%253c", "<").replace("%253e", ">");
        try {
            html = URLDecoder.decode(html, StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            // log error unsupported
            logger.error("unsupported encode", e);
        } catch (IllegalArgumentException e1) {
            // ignore
            logger.warn(MessageFormat.format("unescape html error:{1}", html), e1);
        }


        html = StringEscapeUtils.unescapeHtml4(html);
        return html;
    }

    public static void main(String[] args) {
        XssDetector detector = XssDetector.getNormalDetector();
        String xml = " java script : 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";
        boolean suc = false;
        // -------- 必须全部不通过 -------------
        System.out.println(" -------- 必须全部不通过 -------------");
        xml = "</div>< img src=1 onerror=\"alert('abc')\"></img><div>";
        suc = detector.passXssCheck(xml);
        System.out.println(suc);

        // -------- 必须全部通过 -------------
        System.out.println(" -------- 必须全部通过 -------------");
        // 正常提交
        xml = "{\"birthdayStr\":\"2019-10-26 13:00:00\",\"email\":\"hqh-002@163.com\",\"loginName\":\"xuwenhao\",\"name\":\"徐文浩\",\"orgId\":\"1\",\"roleIds\":\"\",\"sex\":\"男\",\"state\":\"1\",\"sysEnabled\":0,\"tel\":\"18199999999\",\"userId\":\"1\"}";
        suc = detector.passXssCheck(xml);
        System.out.println(suc);

        //网页直接复制的内容页面源代码：
        xml = "<p data-spm-anchor-id=\"0.0.0.i1\">心脏骤停时，人体器官供血也会同步停止，生命随之停止。此时越早急救，患者存活的可能性越大！心肺复苏什么时候做？怎么做？跟着北京大学第三医院急诊科主任医师马青变来学习！</p>\n" +
                "<p class=\"photo_img_20190808\"><img src=\"http://p2.img.cctvpic.com/photoworkspace/contentimg/2019/12/10/2019121011213474248.jpg\" alt=\"\" data-spm-anchor-id=\"0.0.0.i5\" /></p>\n" +
                "<p>　　首先暴露患者胸部，找到正确的按压位置，将双手交叉，手掌掌根处放在胸骨上，腕关节、 肘关节、肩关节保持在一条直线上，垂直于胸壁，用身体力量进行按压。</p>\n" +
                "<p>　　按压过程中双手的掌根部不能离开胸壁，需要保持紧贴的状态。</p>\n" +
                "<p class=\"photo_img_20190808\"> </p>\n" +
                "<p>　　心肺复苏频率要求为1分钟100次～120次，即1秒钟需要摁两下。</p>\n" +
                "<p>　　每次按压的深度要保持5厘米到6厘米，即成年人使用较大力气按压能够达到效果。</p>\n" +
                "<p class=\"photo_img_20190808\"> </p>\n" +
                "<p>　　急救是生命的接力赛，在有体力的情况下，心肺复苏需要一直做到120急救人员到达。</p>";
        suc = detector.passXssCheck(xml);
        System.out.println(suc);

        //文章内容中加入粗体的链接：
        xml = "<p>     外地专程到北京求医，<a href=\"http://www.baidu.com\" target=\"_blank\" rel=\"noopener\"><strong>出站</strong></a>偶<strong><a href=\"https://www.cnys.com/article/list_5_4.html\" target=\"_blank\" rel=\"noopener\">遇热</a></strong>心人，带患者到特定诊所就医，看似热心的背后有何隐情？患者疾病不同却被带到同一诊所，花费千元到上万不等，结果竟是病情依然如故。宣称包治百病，实则没有行医资质，非法执业的医生，高额的医药费，“医托”诈骗陷阱该如何辨别？</p>";
        suc = detector.passXssCheck(xml);
        System.out.println(suc);

        //文章中同时加入粗体和jpg图片：
        xml = "<p>　　最近“抗糖化”成了一个非常火<strong>的美</strong>容名词，不少年轻人一时间似乎都在“抗糖化”。市面上的抗糖化产品也多了起来，但其实很多跟风加入抗糖化大军的朋友，并没有弄明白“抗糖化”到底是什么。</p>\n" +
                "<p><img src=\"api/file/image/admin/1576726806841.jpg\" alt=\"\" width=\"500\" height=\"282\" /></p>\n" +
                "<p> </p>";
        suc = detector.passXssCheck(xml);
        System.out.println(suc);

        // 文章中同时加入粗体和对齐符号：
        xml = "<p data-spm-anchor-id=\"0.0.0.i1\">心脏骤停时，人<strong>体器官供</strong>血也会同步停止，生命随之停止。此时越早急救，患者存活的可能性越大！心肺复苏什么时候做？怎么做？跟着北京大学第三医院急诊科主任医师马青变来学习！</p>\n" +
                "<p class=\"photo_img_20190808\" style=\"text-align: right;\">首先暴露患者胸部，找到正确的按压位置，将双手交叉，手掌掌根处放在胸骨上，腕关节、 肘关节、肩关节保持在一条直线上，垂直于胸壁，用身体力量进行按压。</p>\n" +
                "<p class=\"photo_img_20190808\"> </p>";
        suc = detector.passXssCheck(xml);
        System.out.println(suc);

        //文章中同时加入粗体和删除字符符号：
        xml = "<p data-spm-anchor-id=\"0.0.0.i1\">心脏骤停时，人<strong>体器官供</strong>血也会同步停止，生命随之停止。此时越早急救，患者存活的可能性越大！心肺复苏什么时候做？怎么做？跟着北京大学第三医院急诊科主任医师马青变来学习！</p>\n" +
                "<p class=\"photo_img_20190808\">首先暴露患者胸部，找到正确的按压位置，将<span style=\"text-decoration: line-through;\">双手交</span>叉，手掌掌根处放在胸骨上，腕关节、 肘关节、肩关节保持在一条直线上，垂直于胸壁，用身体力量进行按压。</p>\n" +
                "<p class=\"photo_img_20190808\"> </p>";
        suc = detector.passXssCheck(xml);
        System.out.println(suc);

        //文章中同时加入删除字符符号和图片：
        xml = "<p>　　最近“抗糖化”成了一个非常火的美容名词，不少年轻<span style=\"text-decoration: line-through;\">人一时间似</span>乎都在“抗糖化”。市面上的抗糖化产品也多了起来，但其实很多跟风加入抗糖化大军的朋友，并没有弄明白“抗糖化”到底是什么。</p>\n" +
                "<p><img src=\"api/file/image/admin/1576726806841.jpg\" alt=\"\" width=\"500\" height=\"282\" /></p>" +
                "<p> </p>";
        suc = detector.passXssCheck(xml);
        System.out.println(suc);

        // 文章中加入视频文件：
        xml = "<p><video controls=\"controls\" width=\"300\" height=\"150\">" +
                "<source src=\"api/file/media/download/admin/1576741670153.mp4\" type=\"video/mp4\" /></video></p>";
        suc = detector.passXssCheck(xml);
        System.out.println(suc);
    }
}
